/*
Part of the GUI for Processing library
http://www.lagers.org.uk/g4p/index.html
http://gui4processing.googlecode.com/svn/trunk/
Copyright (c) 2008-12 Peter Lager
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA
*/
package g4p_controls;
import g4p_controls.HotSpot.HSarc;
import g4p_controls.HotSpot.HScircle;
import processing.core.PApplet;
import processing.core.PGraphicsJava2D;
import processing.event.MouseEvent;
/**
* The provides an extremely configurable GUI knob controller. GKnob
* inherits from GValueControl so you should read the documentation
* for that class as it also applies to GKnob. <br><br>
*
* Configurable options <br>
* Knob size but it must be circular <br>
* Start and end of rotation arc. <br>
* Bezel width with tick marks <br>
* User defined value limits (i.e. the range of values returned <br>
* <br>
* Range of values associated with rotating the knob <br>
* Rotation is controlled by mouse movement - 3 modes available <br>
* (a) angular - drag round knob center <br>
* (b) horizontal - drag left or right <br>
* (c) vertical - drag up or down <br>
* User can specify mouse sensitivity for modes (b) and (c)
* Use can specify easing to give smoother rotation
*
* <b>Note</b>: Angles are measured clockwise starting in the positive x direction i.e.
* <pre>
* 270
* |
* 180 --+-- 0
* |
* 90
* </pre>
*
* @author Peter Lager
*
*/
public class GKnob extends GValueControl {
protected float startAng = 110, endAng = 70;
protected int mode = CTRL_HORIZONTAL;
protected boolean showTrack = true;
protected float bezelRadius, bezelWidth, gripRadius;
protected boolean overIncludesBezel = true;
protected float sensitivity = 1.0f;
protected boolean drawArcOnly = false;
protected boolean mouseOverArcOnly = false;
protected float startMouseX, startMouseY;
protected float lastMouseAngle, mouseAngle;
// corresponds to target and current values
// parametricTarget
protected float angleTarget, lastAngleTarget;
/**
* Will create the a circular knob control that fits the rectangle define by
* the values passed as parameters. <br>
* The knob has two zones the outer bezel and the inner gripper. The radius of
* the outer bezel is calculated from <br>
* <pre>bezel radius = min(width, height)/2 - 2<pre><br>
* The radius of the inner griper radius is calculated from the bezel radius
* and the last parameter. <br>
* <pre>grip radius = bezel radiius * gripAmount </pre><br>
* The gripAmount should be in te range 0.0 to 1.0 inclusive. The actual value
* will be constrained to that range. <br>
*
* @param theApplet
* @param p0
* @param p1
* @param p2
* @param p3
* @param gripAmount must be >=0.0 and <=1.0
*/
public GKnob(PApplet theApplet, float p0, float p1, float p2, float p3, float gripAmount) {
super(theApplet, p0, p1, p2, p3);
bezelRadius = Math.min(width, height) / 2 - 2;
setGripAmount(gripAmount);
buffer = (PGraphicsJava2D) winApp.createGraphics((int)width, (int)height, PApplet.JAVA2D);
setTurnRange(startAng, endAng);
// valuePos and valueTarget will start at 0.5;
lastAngleTarget = angleTarget = scaleValueToAngle(parametricTarget);
hotspots = new HotSpot[]{
new HScircle(1, width/2, height/2, gripRadius)
};
z = Z_SLIPPY;
epsilon = 0.98f / (endAng - startAng);
showTicks = true;
// Now register control with applet
createEventHandler(G4P.sketchApplet, "handleKnobEvents",
new Class<?>[]{ GValueControl.class, GEvent.class },
new String[]{ "knob", "event" }
);
registeredMethods = PRE_METHOD | DRAW_METHOD | MOUSE_METHOD ;
cursorOver = HAND;
G4P.addControl(this);
}
/**
* The radius of the inner griper radius is calculated from the bezel radius
* and the parameter gripAmount using <br>
* <pre>grip radius = bezel radiius * gripAmount </pre><br>
* The gripAmount should be in te range 0.0 to 1.0 inclusive. The actual value
* will be constrained to that range. <br>
*
* @param gripAmount must be >=0.0 and <=1.0
*/
public void setGripAmount(float gripAmount){
gripAmount = PApplet.constrain(gripAmount, 0.0f, 1.0f);
gripRadius = bezelRadius * gripAmount;
if(gripRadius < 2.0f) gripRadius = 0.0f;
bezelWidth = bezelRadius - gripRadius;
bufferInvalid = true;
}
protected void calculateHotSpot(){
float overRad = (this.overIncludesBezel) ? bezelRadius : gripRadius;
if(mouseOverArcOnly)
hotspots[0] = new HSarc(1, width/2 , height/2, overRad, startAng, endAng); // over grip
else
hotspots[0] = new HScircle(1, width/2, height/2, overRad);
}
/**
* For a particular normalised value calculate the angle (degrees)
*
* @param v
* @return the needle angle for the given value
*/
protected float scaleValueToAngle(float v){
float a = startAng + v * (endAng - startAng);
return a;
}
/**
* Calculates the knob angle based on the normalised value.
*
* @param a
*/
protected float calcAngletoValue(float a){
if(a < startAng)
a += 360;
float v = (a - startAng) / (endAng - startAng);
return v;
}
/**
* Set the value for the slider. <br>
* The user must ensure that the value is valid for the slider range.
* @param v
*/
public void setValue(float v){
super.setValue(v);
angleTarget = scaleValueToAngle(parametricTarget);
}
/**
* Whether or not to show the circular progress bar.
* @param showTrack true for visible
*/
public void setShowTrack(boolean showTrack){
if(this.showTrack != showTrack){
this.showTrack = showTrack;
bufferInvalid = true;
}
}
/**
* Are we showing the the value track bar.
*/
public boolean isShowTrack(){
return showTrack;
}
/**
* Whether to include the bezel when deciding when the mouse is over.
* @param overBezel true if bezel inclded.
*/
public void setIncludeOverBezel(boolean overBezel){
overIncludesBezel = overBezel;
calculateHotSpot();
}
/**
* Is the bezel included when considering when the mouse is over.
* @return true if included.
*/
public boolean isIncludeOverBezel(){
return overIncludesBezel;
}
/**
* Decides when the knob will respond to the mouse buttons. If set to true
* it will only respond when ver the arc made by the start and end angles. If
* false it will be the full circle.
* @param arcOnly
*/
public void setOverArcOnly(boolean arcOnly){
mouseOverArcOnly = arcOnly;
calculateHotSpot();
}
/**
* Does the mouse only respond when over the arc?
* @return true = yes
*/
public boolean isOverArcOnly(){
return mouseOverArcOnly;
}
/**
* Convenience method to set both the show and the mouse over arc only properties
* for this knob
* @param over_arc_only mouse over arc only?
* @param draw_arc_only draw arc only?
* @param overfullsize include bezel in mouse over calculations?
*/
public void setArcPolicy(boolean over_arc_only, boolean draw_arc_only, boolean overfullsize){
this.mouseOverArcOnly = over_arc_only;
setShowArcOnly(draw_arc_only);
overIncludesBezel = overfullsize;
calculateHotSpot();
}
/**
* This will decide whether the knob is draw as a full circle or as an arc.
*
* @param arcOnly true for arc only
*/
public void setShowArcOnly(boolean arcOnly){
if(drawArcOnly != arcOnly){
drawArcOnly = arcOnly;
bufferInvalid = true;
}
}
/**
* Are we showing arc only?
* @return true = yes
*/
public boolean isShowArcOnly(){
return drawArcOnly;
}
public void mouseEvent(MouseEvent event){
if(!visible || !enabled || !available) return;
calcTransformedOrigin(winApp.mouseX, winApp.mouseY);
currSpot = whichHotSpot(ox, oy);
// Normalise ox and oy to the centre of the knob
ox -= width/2;
oy -= height/2;
// currSpot == 1 for text display area
if(currSpot >= 0 || focusIsWith == this)
cursorIsOver = this;
else if(cursorIsOver == this)
cursorIsOver = null;
switch(event.getAction()){
case MouseEvent.PRESS:
if(focusIsWith != this && currSpot > -1 && z > focusObjectZ()){
startMouseX = ox;
startMouseY = oy;
lastMouseAngle = mouseAngle = getAngleFromUser(ox, oy);
offset = scaleValueToAngle(parametricTarget) - mouseAngle;
takeFocus();
}
break;
case MouseEvent.RELEASE:
if(focusIsWith == this){
loseFocus(null);
}
// Correct for sticky ticks if needed
if(stickToTicks)
parametricTarget = findNearestTickValueTo(parametricTarget);
dragging = false;
break;
case MouseEvent.DRAG:
if(focusIsWith == this){
mouseAngle = getAngleFromUser(ox, oy);
if(mouseAngle != lastMouseAngle){
float deltaMangle = mouseAngle - lastMouseAngle;
// correct when we go over zero degree position
if(deltaMangle < -180)
deltaMangle += 360;
else if(deltaMangle > 180)
deltaMangle -= 360;
// Calculate and adjust new needle angle so it is in the range aLow >>> aHigh
angleTarget = constrainToTurnRange(angleTarget + deltaMangle);
parametricTarget = calcAngletoValue(angleTarget);
// Update offset for use with angular mouse control
offset += (angleTarget - lastAngleTarget - deltaMangle);
// Remember target needle and mouse angles
lastAngleTarget = angleTarget;
lastMouseAngle = mouseAngle;
}
}
break;
}
}
public void draw(){
if(!visible) return;
// Update buffer if invalid
updateBuffer();
winApp.pushStyle();
winApp.pushMatrix();
// Perform the rotation
winApp.translate(cx, cy);
winApp.rotate(rotAngle);
winApp.pushMatrix();
// Move matrix to line up with top-left corner
winApp.translate(-halfWidth, -halfHeight);
// Draw buffer
winApp.imageMode(PApplet.CORNER);
if(alphaLevel < 255)
winApp.tint(TINT_FOR_ALPHA, alphaLevel);
winApp.image(buffer, 0, 0);
winApp.popMatrix();
// Value labels
if(children != null){
for(GAbstractControl c : children)
c.draw();
}
winApp.popMatrix();
winApp.popStyle();
}
protected void updateBuffer(){
double a, sina, cosa;
float tickLength;
if(bufferInvalid) {
bufferInvalid = false;
buffer.beginDraw();
buffer.ellipseMode(PApplet.CENTER);
// Back ground colour
// Back ground colour
if(opaque == true)
buffer.background(palette[6]);
else
buffer.background(buffer.color(255,0));
buffer.translate(width/2, height/2);
buffer.noStroke();
float anglePos = scaleValueToAngle(parametricPos);
if(bezelWidth > 0){
// Draw bezel, track, ticks etc
buffer.noStroke();
buffer.fill(palette[5]);
if(drawArcOnly)
buffer.arc(0,0,2*bezelRadius, 2*bezelRadius, PApplet.radians(startAng), PApplet.radians(endAng));
else
buffer.ellipse(0,0,2*bezelRadius, 2*bezelRadius);
// Since we have a bezel test for ticks
if(showTicks){
buffer.noFill();
buffer.strokeWeight(1.6f);
buffer.stroke(palette[3]);
float deltaA = (endAng - startAng)/(nbrTicks - 1);
for(int t = 0; t < nbrTicks; t++){
tickLength = gripRadius + ((t == 0 || t == nbrTicks - 1) ? bezelWidth : bezelWidth * 0.8f);
a = Math.toRadians(startAng + t * deltaA);
sina = Math.sin(a);
cosa = Math.cos(a);
buffer.line((float)(gripRadius * cosa), (float)(gripRadius * sina), (float)(tickLength * cosa), (float)(tickLength * sina));
}
}
// draw track?
if(showTrack){
buffer.noStroke();
buffer.fill(palette[14]);
buffer.arc(0,0, 2*(gripRadius + bezelWidth * 0.5f), 2*(gripRadius + bezelWidth * 0.5f), PApplet.radians(startAng), PApplet.radians(anglePos));
}
}
// draw grip (inner) part of knob
buffer.strokeWeight(1.6f);
buffer.stroke(palette[2]); // was 14
buffer.fill(palette[2]);
if(drawArcOnly)
buffer.arc(0,0,2*gripRadius, 2*gripRadius, PApplet.radians(startAng), PApplet.radians(endAng));
else
buffer.ellipse(0,0,2*gripRadius, 2*gripRadius);
// Draw needle
buffer.noFill();
buffer.stroke(palette[14]);
buffer.strokeWeight(3);
a = Math.toRadians(anglePos);
sina = Math.sin(a);
cosa = Math.cos(a);
buffer.line(0, 0, (float)(gripRadius * cosa), (float)(gripRadius * sina));
buffer.endDraw();
}
}
/**
* Get the current mouse controller mode possible values are <br>
* GKnob.CTRL_ANGULAR or GKnob.CTRL_HORIZONTAL) orGKnob.CTRL_VERTICAL
* @return the mode
*/
public int getTurnMode() {
return mode;
}
/**
* Set the mouse control mode to use, acceptable values are <br>
* GKnob.CTRL_ANGULAR or GKnob.CTRL_HORIZONTAL) orGKnob.CTRL_VERTICAL any
* other value will be ignored.
* @param mode the mode to set
*/
public void setTurnMode(int mode) {
switch(mode){
case CTRL_ANGULAR:
case CTRL_HORIZONTAL:
case CTRL_VERTICAL:
this.mode = mode;
}
}
/**
* This gets the sensitivity to be used in modes CTRL_HORIZONTAL and CTRL_VERTICAL
* @return the sensitivity
*/
public float getSensitivity() {
return sensitivity;
}
/**
* This gets the sensitivity to be used in modes CTRL_HORIZONTAL and CTRL_VERTICAL <br>
* A value of 1 is 1 degree per pixel and a value of 2 is 2 degrees per pixel. <br>
* @param sensitivity the sensitivity to set
*/
public void setSensitivity(float sensitivity) {
this.sensitivity = (sensitivity < 0.1f) ? 0.1f : sensitivity;
}
/**
* Calculates the 'angle' from the current mouse position based on the type
* of 'controller' set.
* @param px the distance from the knob centre in the x direction
* @param py the distance from the knob centre in the y direction
* @return the unconstrained angle
*/
protected float getAngleFromUser(float px, float py){
float degs = 0;
switch(mode){
case CTRL_ANGULAR:
degs = calcRealAngleFromXY(ox, oy);
break;
case CTRL_HORIZONTAL:
degs = sensitivity * (px - startMouseX);
break;
case CTRL_VERTICAL:
degs = sensitivity * (py - startMouseY);
break;
}
return degs;
}
/**
* Set the limits for the range of valid rotation angles for the knob.
*
* @param start the range start angle in degrees
* @param end the range end angle in degrees
*/
public void setTurnRange(float start, float end){
start = constrain360(start);
end = constrain360(end);
startAng = start;
endAng = (startAng >= end) ? end + 360 : end;
setValue(getValueF());
// anglePos = angleTarget;
bufferInvalid = true;
}
/**
* Determines whether an angle is within the knob
* rotation range.
* @param a the angle in degrees
* @return true is angle is within rotation range else false
*/
protected boolean isInTurnRange(float a){
a = constrain360(a);
if(a < startAng)
a += 360;
return (a >= startAng && a <= endAng);
}
/**
* Accept an angle and constrain it to the knob angle range.
*
* @param a
* @return the constrained angle
*/
protected float constrainToTurnRange(float a){
if(a < startAng)
a = startAng;
else if(a > endAng)
a = endAng;
return a;
}
/**
* Accept an angle and constrain it to the range 0-360
* @param a
* @return the constrained angle
*/
protected float constrain360(float a){
while(a < 0)
a += 360;
while(a > 360)
a -= 360;
return a;
}
/**
* Calculate the angle to the knob centre making sure it is in
* the range 0-360
* @param px
* @param py
* @return the angle from the knob centre to the specified point.
*/
protected float calcRealAngleFromXY(float px, float py){
float a = (float) Math.toDegrees(Math.atan2(py, px));
if(a < 0)
a += 360;
return a;
}
}